Skip to content

Release get_free_port() reservations not tied to a Server's primary port#475

Merged
esabol merged 1 commit into
gearman:masterfrom
p-alik:fix-issue-474-auxiliary-port-release
Jul 17, 2026
Merged

Release get_free_port() reservations not tied to a Server's primary port#475
esabol merged 1 commit into
gearman:masterfrom
p-alik:fix-issue-474-auxiliary-port-release

Conversation

@p-alik

@p-alik p-alik commented Jul 17, 2026

Copy link
Copy Markdown
Collaborator

Summary

#472 fixed the primary port-reservation race in libtest::Server::start(): get_free_port() now keeps its reservation socket bound until release_port() is called, and Server::start() releases its own _port once the server is confirmed up (or definitively failed).

That fix only releases the Server's own _port. Several call sites reserve a port for something other than a Server object's primary port and never released it — the reservation socket now leaks (stays bound) for the rest of that test binary's process lifetime:

  • tests/context.h's Context, tests/cycle.cc's cycle_context_st, and tests/round_robin.cc's Context all overwrite their stored port in reset() with a fresh get_free_port() value without releasing the old one first, and never released the final port on destruction.
  • tests/httpd_test.cc reserves http_port, passed only as --http-port= to a gearmand instance whose own primary port is default_port() — the Server never knows about http_port.
  • tests/gearmand.cc's long_keepalive_start_TEST and config_file_SIMPLE_TEST reserve a port for --check-args validation runs that never actually bind it (no Server involved at all).
  • libtest/unittest.cc's get_free_port_TEST exercises get_free_port() directly and never releases what it reserves.

Fix

Release the port explicitly wherever the above patterns discard/overwrite/finish with a reservation:

  • reset()-style methods now call release_port() on the old port before generating a new one; destructors now release the current port directly instead of wastefully fetching a new one just to abandon it (round_robin.cc's destructor previously called reset(), which did exactly that).
  • httpd_test.cc releases http_port right after server_startup() returns (success or failure) — by then gearmand has either bound it or failed outright.
  • gearmand.cc's two --check-args tests release the port immediately after formatting it into the CLI/config argument, since --check-args never binds.
  • get_free_port_TEST releases each of its throwaway reservations at the end.

default_port()'s single process-wide reservation is intentionally left untouched — callers throughout the test suite rely on it staying valid for the whole process lifetime, so releasing it would be a behavior change, not a leak fix.

Test plan

  • Full rebuild of all affected binaries (t/unittest, t/cycle, t/round_robin, t/gearmand, t/httpd, plus everything else that includes tests/context.h: t/drizzle, t/memcached, t/redis, t/mysql, t/tokyocabinet, t/postgres, t/ephemeral) — all compile clean.
  • Ran every affected binary individually — all pass (backends without a local install, e.g. drizzle/memcached/mysql/postgres, skip as expected; t/unittest's only failures are pre-existing environment artifacts unrelated to this change — getenv() checks that only pass under make check's harness, and a /bin/echo-path issue specific to this NixOS dev machine).
  • Compared 4x-concurrent t/cycle and a heavier 16-way mix (protocol/worker/round_robin/cycle) against the pre-fix baseline on the same machine: an occasional t/cycle flake under the heaviest combination reproduces identically with or without this change, confirming it's pre-existing high-load contention in this environment, not a regression from this PR.

Fixes #474

🤖 Generated with Claude Code

Since gearman#472, get_free_port() leaves its reservation socket bound until
release_port() is called, and Server::start() only releases its own
_port. Several call sites reserve a port for something other than a
Server's primary port and never released it, leaking the reservation
socket for the rest of the test process:

- Context (tests/context.h) and cycle_context_st (tests/cycle.cc) and
  round_robin.cc's Context overwrite their stored port in reset()
  without releasing the old one first, and never release the final
  port on destruction.
- httpd_test.cc reserves an http_port passed only as --http-port= to
  a server whose own primary port is default_port().
- gearmand.cc's long_keepalive_start_TEST and config_file_SIMPLE_TEST
  reserve a port for --check-args validation runs that never bind it.
- unittest.cc's get_free_port_TEST exercises get_free_port() directly
  without ever releasing what it reserves.

default_port()'s single process-wide reservation is intentionally left
untouched, since callers rely on it staying valid for the process's
whole lifetime.

Fixes gearman#474

Signed-off-by: Alexei Pastuchov <info@maximka.de>

@esabol esabol left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks fine. I approve these changes.

@esabol
esabol merged commit 6d66018 into gearman:master Jul 17, 2026
19 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

libtest: ports obtained via get_free_port() for secondary/auxiliary uses are never released

2 participants